<?php

if (!defined('BASEPATH'))
    exit('No direct script access allowed');

/**
 * GAPI - Google Analytics PHP Interface
 * 
 * http://code.google.com/p/gapi-google-analytics-php-interface/
 * 
 * @copyright Stig Manning 2009
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Stig Manning <stig@sdm.co.nz>
 * @version 1.3
 * 
 */
class gapi {

    const http_interface = 'auto'; //'auto': autodetect, 'curl' or 'fopen'
    const client_login_url = 'https://www.google.com/accounts/ClientLogin';
    const account_data_url = 'https://www.google.com/analytics/feeds/accounts/default';
    const report_data_url = 'https://www.google.com/analytics/feeds/data';
    const interface_name = 'GAPI-1.3';
    const dev_mode = false;

    private $auth_token = null;
    private $account_entries = array();
    private $account_root_parameters = array();
    private $report_aggregate_metrics = array();
    private $report_root_parameters = array();
    private $results = array();

    /**
     * Constructor function for all new gapi instances
     * 
     * Set up authenticate with Google and get auth_token
     *
     * @param String $email
     * @param String $password
     * @param String $token
     * @return gapi
     */
    public function __construct($email, $password, $token = null) {
        if ($token !== null) {
            $this->auth_token = $token;
        } else {
            $this->authenticateUser($email, $password);
        }
    }

    /**
     * Return the auth token, used for storing the auth token in the user session
     *
     * @return String
     */
    public function getAuthToken() {
        return $this->auth_token;
    }

    /**
     * Request account data from Google Analytics
     *
     * @param Int $start_index OPTIONAL: Start index of results
     * @param Int $max_results OPTIONAL: Max results returned
     */
    public function requestAccountData($start_index = 1, $max_results = 20) {
        $response = $this->httpRequest(gapi::account_data_url, array('start-index' => $start_index, 'max-results' => $max_results), null, $this->generateAuthHeader());

        if (substr($response['code'], 0, 1) == '2') {
            return $this->accountObjectMapper($response['body']);
        } else {
            throw new Exception('GAPI: Failed to request account data. Error: "' . strip_tags($response['body']) . '"');
        }
    }

    /**
     * Request report data from Google Analytics
     *
     * $report_id is the Google report ID for the selected account
     * 
     * $parameters should be in key => value format
     * 
     * @param String $report_id
     * @param Array $dimensions Google Analytics dimensions e.g. array('browser')
     * @param Array $metrics Google Analytics metrics e.g. array('pageviews')
     * @param Array $sort_metric OPTIONAL: Dimension or dimensions to sort by e.g.('-visits')
     * @param String $filter OPTIONAL: Filter logic for filtering results
     * @param String $start_date OPTIONAL: Start of reporting period
     * @param String $end_date OPTIONAL: End of reporting period
     * @param Int $start_index OPTIONAL: Start index of results
     * @param Int $max_results OPTIONAL: Max results returned
     */
    public function requestReportData($report_id, $dimensions, $metrics, $sort_metric = null, $filter = null, $start_date = null, $end_date = null, $start_index = 1, $max_results = 30) {
        $parameters = array('ids' => 'ga:' . $report_id);

        if (is_array($dimensions)) {
            $dimensions_string = '';
            foreach ($dimensions as $dimesion) {
                $dimensions_string .= ',ga:' . $dimesion;
            }
            $parameters['dimensions'] = substr($dimensions_string, 1);
        } else {
            $parameters['dimensions'] = 'ga:' . $dimensions;
        }

        if (is_array($metrics)) {
            $metrics_string = '';
            foreach ($metrics as $metric) {
                $metrics_string .= ',ga:' . $metric;
            }
            $parameters['metrics'] = substr($metrics_string, 1);
        } else {
            $parameters['metrics'] = 'ga:' . $metrics;
        }

        if ($sort_metric == null && isset($parameters['metrics'])) {
            $parameters['sort'] = $parameters['metrics'];
        } elseif (is_array($sort_metric)) {
            $sort_metric_string = '';

            foreach ($sort_metric as $sort_metric_value) {
                //Reverse sort - Thanks Nick Sullivan
                if (substr($sort_metric_value, 0, 1) == "-") {
                    $sort_metric_string .= ',-ga:' . substr($sort_metric_value, 1); // Descending
                } else {
                    $sort_metric_string .= ',ga:' . $sort_metric_value; // Ascending
                }
            }

            $parameters['sort'] = substr($sort_metric_string, 1);
        } else {
            if (substr($sort_metric, 0, 1) == "-") {
                $parameters['sort'] = '-ga:' . substr($sort_metric, 1);
            } else {
                $parameters['sort'] = 'ga:' . $sort_metric;
            }
        }

        if ($filter != null) {
            $filter = $this->processFilter($filter);
            if ($filter !== false) {
                $parameters['filters'] = $filter;
            }
        }

        if ($start_date == null) {
            $start_date = date('Y-m-d', strtotime('1 month ago'));
        }

        $parameters['start-date'] = $start_date;

        if ($end_date == null) {
            $end_date = date('Y-m-d');
        }

        $parameters['end-date'] = $end_date;


        $parameters['start-index'] = $start_index;
        $parameters['max-results'] = $max_results;

        $parameters['prettyprint'] = gapi::dev_mode ? 'true' : 'false';

        $response = $this->httpRequest(gapi::report_data_url, $parameters, null, $this->generateAuthHeader());

        //HTTP 2xx
        if (substr($response['code'], 0, 1) == '2') {
            return $this->reportObjectMapper($response['body']);
        } else {
            throw new Exception('GAPI: Failed to request report data. Error: "' . strip_tags($response['body']) . '"');
        }
    }

    /**
     * Process filter string, clean parameters and convert to Google Analytics
     * compatible format
     * 
     * @param String $filter
     * @return String Compatible filter string
     */
    protected function processFilter($filter) {
        $valid_operators = '(!~|=~|==|!=|>|<|>=|<=|=@|!@)';

        $filter = preg_replace('/\s\s+/', ' ', trim($filter)); //Clean duplicate whitespace
        $filter = str_replace(array(',', ';'), array('\,', '\;'), $filter); //Escape Google Analytics reserved characters
        $filter = preg_replace('/(&&\s*|\|\|\s*|^)([a-z]+)(\s*' . $valid_operators . ')/i', '$1ga:$2$3', $filter); //Prefix ga: to metrics and dimensions
        $filter = preg_replace('/[\'\"]/i', '', $filter); //Clear invalid quote characters
        $filter = preg_replace(array('/\s*&&\s*/', '/\s*\|\|\s*/', '/\s*' . $valid_operators . '\s*/'), array(';', ',', '$1'), $filter); //Clean up operators

        if (strlen($filter) > 0) {
            return urlencode($filter);
        } else {
            return false;
        }
    }

    /**
     * Report Account Mapper to convert the XML to array of useful PHP objects
     *
     * @param String $xml_string
     * @return Array of gapiAccountEntry objects
     */
    protected function accountObjectMapper($xml_string) {
        $xml = simplexml_load_string($xml_string);

        $this->results = null;

        $results = array();
        $account_root_parameters = array();

        //Load root parameters

        $account_root_parameters['updated'] = strval($xml->updated);
        $account_root_parameters['generator'] = strval($xml->generator);
        $account_root_parameters['generatorVersion'] = strval($xml->generator->attributes());

        $open_search_results = $xml->children('http://a9.com/-/spec/opensearchrss/1.0/');

        foreach ($open_search_results as $key => $open_search_result) {
            $report_root_parameters[$key] = intval($open_search_result);
        }

        $account_root_parameters['startDate'] = strval($google_results->startDate);
        $account_root_parameters['endDate'] = strval($google_results->endDate);

        //Load result entries

        foreach ($xml->entry as $entry) {
            $properties = array();
            foreach ($entry->children('http://schemas.google.com/analytics/2009')->property as $property) {
                $properties[str_replace('ga:', '', $property->attributes()->name)] = strval($property->attributes()->value);
            }

            $properties['title'] = strval($entry->title);
            $properties['updated'] = strval($entry->updated);

            $results[] = new gapiAccountEntry($properties);
        }

        $this->account_root_parameters = $account_root_parameters;
        $this->results = $results;

        return $results;
    }

    /**
     * Report Object Mapper to convert the XML to array of useful PHP objects
     *
     * @param String $xml_string
     * @return Array of gapiReportEntry objects
     */
    protected function reportObjectMapper($xml_string) {
        $xml = simplexml_load_string($xml_string);

        $this->results = null;
        $results = array();

        $report_root_parameters = array();
        $report_aggregate_metrics = array();

        //Load root parameters

        $report_root_parameters['updated'] = strval($xml->updated);
        $report_root_parameters['generator'] = strval($xml->generator);
        $report_root_parameters['generatorVersion'] = strval($xml->generator->attributes());

        $open_search_results = $xml->children('http://a9.com/-/spec/opensearchrss/1.0/');

        foreach ($open_search_results as $key => $open_search_result) {
            $report_root_parameters[$key] = intval($open_search_result);
        }

        $google_results = $xml->children('http://schemas.google.com/analytics/2009');

        foreach ($google_results->dataSource->property as $property_attributes) {
            $report_root_parameters[str_replace('ga:', '', $property_attributes->attributes()->name)] = strval($property_attributes->attributes()->value);
        }

        $report_root_parameters['startDate'] = strval($google_results->startDate);
        $report_root_parameters['endDate'] = strval($google_results->endDate);

        //Load result aggregate metrics

        foreach ($google_results->aggregates->metric as $aggregate_metric) {
            $metric_value = strval($aggregate_metric->attributes()->value);

            //Check for float, or value with scientific notation
            if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value)) {
                $report_aggregate_metrics[str_replace('ga:', '', $aggregate_metric->attributes()->name)] = floatval($metric_value);
            } else {
                $report_aggregate_metrics[str_replace('ga:', '', $aggregate_metric->attributes()->name)] = intval($metric_value);
            }
        }

        //Load result entries

        foreach ($xml->entry as $entry) {
            $metrics = array();
            foreach ($entry->children('http://schemas.google.com/analytics/2009')->metric as $metric) {
                $metric_value = strval($metric->attributes()->value);

                //Check for float, or value with scientific notation
                if (preg_match('/^(\d+\.\d+)|(\d+E\d+)|(\d+.\d+E\d+)$/', $metric_value)) {
                    $metrics[str_replace('ga:', '', $metric->attributes()->name)] = floatval($metric_value);
                } else {
                    $metrics[str_replace('ga:', '', $metric->attributes()->name)] = intval($metric_value);
                }
            }

            $dimensions = array();
            foreach ($entry->children('http://schemas.google.com/analytics/2009')->dimension as $dimension) {
                $dimensions[str_replace('ga:', '', $dimension->attributes()->name)] = strval($dimension->attributes()->value);
            }

            $results[] = new gapiReportEntry($metrics, $dimensions);
        }

        $this->report_root_parameters = $report_root_parameters;
        $this->report_aggregate_metrics = $report_aggregate_metrics;
        $this->results = $results;

        return $results;
    }

    /**
     * Authenticate Google Account with Google
     *
     * @param String $email
     * @param String $password
     */
    protected function authenticateUser($email, $password) {
        $post_variables = array(
            'accountType' => 'GOOGLE',
            'Email' => $email,
            'Passwd' => $password,
            'source' => gapi::interface_name,
            'service' => 'analytics'
        );

        $response = $this->httpRequest(gapi::client_login_url, null, $post_variables);

        //Convert newline delimited variables into url format then import to array
        parse_str(str_replace(array("\n", "\r\n"), '&', $response['body']), $auth_token);

        if (substr($response['code'], 0, 1) != '2' || !is_array($auth_token) || empty($auth_token['Auth'])) {
            throw new Exception('GAPI: Failed to authenticate user. Error: "' . strip_tags($response['body']) . '"');
        }

        $this->auth_token = $auth_token['Auth'];
    }

    /**
     * Generate authentication token header for all requests
     *
     * @return Array
     */
    protected function generateAuthHeader() {
        return array('Authorization: GoogleLogin auth=' . $this->auth_token);
    }

    /**
     * Perform http request
     * 
     *
     * @param Array $get_variables
     * @param Array $post_variables
     * @param Array $headers
     */
    protected function httpRequest($url, $get_variables = null, $post_variables = null, $headers = null) {
        $interface = gapi::http_interface;

        if (gapi::http_interface == 'auto') {
            if (function_exists('curl_exec')) {
                $interface = 'curl';
            } else {
                $interface = 'fopen';
            }
        }

        if ($interface == 'curl') {
            return $this->curlRequest($url, $get_variables, $post_variables, $headers);
        } elseif ($interface == 'fopen') {
            return $this->fopenRequest($url, $get_variables, $post_variables, $headers);
        } else {
            throw new Exception('Invalid http interface defined. No such interface "' . gapi::http_interface . '"');
        }
    }

    /**
     * HTTP request using PHP CURL functions
     * Requires curl library installed and configured for PHP
     * 
     * @param Array $get_variables
     * @param Array $post_variables
     * @param Array $headers
     */
    private function curlRequest($url, $get_variables = null, $post_variables = null, $headers = null) {
        $ch = curl_init();

        if (is_array($get_variables)) {
            $get_variables = '?' . str_replace('&amp;', '&', urldecode(http_build_query($get_variables)));
        } else {
            $get_variables = null;
        }

        curl_setopt($ch, CURLOPT_URL, $url . $get_variables);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //CURL doesn't like google's cert

        if (is_array($post_variables)) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_variables);
        }

        if (is_array($headers)) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        }

        $response = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

        curl_close($ch);

        return array('body' => $response, 'code' => $code);
    }

    /**
     * HTTP request using native PHP fopen function
     * Requires PHP openSSL
     *
     * @param Array $get_variables
     * @param Array $post_variables
     * @param Array $headers
     */
    private function fopenRequest($url, $get_variables = null, $post_variables = null, $headers = null) {
        $http_options = array('method' => 'GET', 'timeout' => 3);

        if (is_array($headers)) {
            $headers = implode("\r\n", $headers) . "\r\n";
        } else {
            $headers = '';
        }

        if (is_array($get_variables)) {
            $get_variables = '?' . str_replace('&amp;', '&', urldecode(http_build_query($get_variables)));
        } else {
            $get_variables = null;
        }

        if (is_array($post_variables)) {
            $post_variables = str_replace('&amp;', '&', urldecode(http_build_query($post_variables)));
            $http_options['method'] = 'POST';
            $headers = "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($post_variables) . "\r\n" . $headers;
            $http_options['header'] = $headers;
            $http_options['content'] = $post_variables;
        } else {
            $post_variables = '';
            $http_options['header'] = $headers;
        }

        $context = stream_context_create(array('http' => $http_options));
        $response = @file_get_contents($url . $get_variables, null, $context);

        return array('body' => $response !== false ? $response : 'Request failed, fopen provides no further information', 'code' => $response !== false ? '200' : '400');
    }

    /**
     * Case insensitive array_key_exists function, also returns
     * matching key.
     *
     * @param String $key
     * @param Array $search
     * @return String Matching array key
     */
    public static function array_key_exists_nc($key, $search) {
        if (array_key_exists($key, $search)) {
            return $key;
        }
        if (!(is_string($key) && is_array($search))) {
            return false;
        }
        $key = strtolower($key);
        foreach ($search as $k => $v) {
            if (strtolower($k) == $key) {
                return $k;
            }
        }
        return false;
    }

    /**
     * Get Results
     *
     * @return Array
     */
    public function getResults() {
        if (is_array($this->results)) {
            return $this->results;
        } else {
            return;
        }
    }

    /**
     * Get an array of the metrics and the matchning
     * aggregate values for the current result
     *
     * @return Array
     */
    public function getMetrics() {
        return $this->report_aggregate_metrics;
    }

    /**
     * Call method to find a matching root parameter or 
     * aggregate metric to return
     *
     * @param $name String name of function called
     * @return String
     * @throws Exception if not a valid parameter or aggregate 
     * metric, or not a 'get' function
     */
    public function __call($name, $parameters) {
        if (!preg_match('/^get/', $name)) {
            throw new Exception('No such function "' . $name . '"');
        }

        $name = preg_replace('/^get/', '', $name);

        $parameter_key = gapi::array_key_exists_nc($name, $this->report_root_parameters);

        if ($parameter_key) {
            return $this->report_root_parameters[$parameter_key];
        }

        $aggregate_metric_key = gapi::array_key_exists_nc($name, $this->report_aggregate_metrics);

        if ($aggregate_metric_key) {
            return $this->report_aggregate_metrics[$aggregate_metric_key];
        }

        throw new Exception('No valid root parameter or aggregate metric called "' . $name . '"');
    }

}

/**
 * Class gapiAccountEntry
 * 
 * Storage for individual gapi account entries
 *
 */
class gapiAccountEntry {

    private $properties = array();

    public function __construct($properties) {
        $this->properties = $properties;
    }

    /**
     * toString function to return the name of the account
     *
     * @return String
     */
    public function __toString() {
        if (isset($this->properties['title'])) {
            return $this->properties['title'];
        } else {
            return;
        }
    }

    /**
     * Get an associative array of the properties
     * and the matching values for the current result
     *
     * @return Array
     */
    public function getProperties() {
        return $this->properties;
    }

    /**
     * Call method to find a matching parameter to return
     *
     * @param $name String name of function called
     * @return String
     * @throws Exception if not a valid parameter, or not a 'get' function
     */
    public function __call($name, $parameters) {
        if (!preg_match('/^get/', $name)) {
            throw new Exception('No such function "' . $name . '"');
        }

        $name = preg_replace('/^get/', '', $name);

        $property_key = gapi::array_key_exists_nc($name, $this->properties);

        if ($property_key) {
            return $this->properties[$property_key];
        }

        throw new Exception('No valid property called "' . $name . '"');
    }

}

/**
 * Class gapiReportEntry
 * 
 * Storage for individual gapi report entries
 *
 */
class gapiReportEntry {

    private $metrics = array();
    private $dimensions = array();

    public function __construct($metrics, $dimesions) {
        $this->metrics = $metrics;
        $this->dimensions = $dimesions;
    }

    /**
     * toString function to return the name of the result
     * this is a concatented string of the dimesions chosen
     * 
     * For example:
     * 'Firefox 3.0.10' from browser and browserVersion
     *
     * @return String
     */
    public function __toString() {
        if (is_array($this->dimensions)) {
            return implode(' ', $this->dimensions);
        } else {
            return '';
        }
    }

    /**
     * Get an associative array of the dimesions
     * and the matching values for the current result
     *
     * @return Array
     */
    public function getDimesions() {
        return $this->dimensions;
    }

    /**
     * Get an array of the metrics and the matchning
     * values for the current result
     *
     * @return Array
     */
    public function getMetrics() {
        return $this->metrics;
    }

    /**
     * Call method to find a matching metric or dimension to return
     *
     * @param $name String name of function called
     * @return String
     * @throws Exception if not a valid metric or dimensions, or not a 'get' function
     */
    public function __call($name, $parameters) {
        if (!preg_match('/^get/', $name)) {
            throw new Exception('No such function "' . $name . '"');
        }

        $name = preg_replace('/^get/', '', $name);

        $metric_key = gapi::array_key_exists_nc($name, $this->metrics);

        if ($metric_key) {
            return $this->metrics[$metric_key];
        }

        $dimension_key = gapi::array_key_exists_nc($name, $this->dimensions);

        if ($dimension_key) {
            return $this->dimensions[$dimension_key];
        }

        throw new Exception('No valid metric or dimesion called "' . $name . '"');
    }

}